summaryrefslogtreecommitdiff
path: root/app/api/auth/[...nextauth]
diff options
context:
space:
mode:
Diffstat (limited to 'app/api/auth/[...nextauth]')
-rw-r--r--app/api/auth/[...nextauth]/saml/provider.ts159
-rw-r--r--app/api/auth/[...nextauth]/saml/utils.ts175
2 files changed, 268 insertions, 66 deletions
diff --git a/app/api/auth/[...nextauth]/saml/provider.ts b/app/api/auth/[...nextauth]/saml/provider.ts
index 92099be0..1f891661 100644
--- a/app/api/auth/[...nextauth]/saml/provider.ts
+++ b/app/api/auth/[...nextauth]/saml/provider.ts
@@ -1,5 +1,9 @@
import CredentialsProvider from "next-auth/providers/credentials"
import { getOrCreateSAMLUser, validateSAMLUserData } from '@/lib/users/saml-service'
+import { encode } from 'next-auth/jwt'
+import type { User } from 'next-auth'
+import type { SAMLUser } from './utils'
+import { debugLog, debugError, debugSuccess, debugProcess } from '@/lib/debug-utils'
interface SAMLProviderOptions {
id: string
@@ -28,59 +32,80 @@ export function SAMLProvider(options: SAMLProviderOptions) {
}
},
async authorize(credentials) {
+ debugLog('๐Ÿ” SAMLProvider.authorize called with credentials:', credentials);
+
try {
+ debugLog('๐Ÿ” Checking credentials.user:', {
+ hasCredentials: !!credentials,
+ hasUser: !!credentials?.user,
+ userType: typeof credentials?.user,
+ userValue: credentials?.user?.substring?.(0, 100) + '...'
+ });
+
if (!credentials?.user) {
- console.error('No user data provided')
+ debugError('No user data provided in credentials')
return null
}
- console.log('๐Ÿ” SAML Provider: Processing user data')
+ debugProcess('SAML Provider: Processing user data')
// ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ (UTF-8 ์ฒ˜๋ฆฌ ๊ฐœ์„ )
const userDataString = credentials.user
- console.log('๐Ÿ”ค Raw user data string:', userDataString.substring(0, 200) + '...')
+ debugLog('๐Ÿ”ค Raw user data string:', userDataString.substring(0, 200) + '...')
- const userData = JSON.parse(userDataString)
+ let userData;
+ try {
+ userData = JSON.parse(userDataString);
+ debugSuccess('JSON parsing successful:', userData);
+ } catch (parseError) {
+ debugError('JSON parsing failed:', parseError);
+ debugError('Raw string that failed to parse:', userDataString);
+ return null;
+ }
// ํŒŒ์‹ฑ๋œ ๋ฐ์ดํ„ฐ์˜ UTF-8 ํ™•์ธ
- console.log('๐Ÿ”ค Parsed user data UTF-8 check:', {
+ debugLog('๐Ÿ”ค Parsed user data UTF-8 check:', {
name: userData.name,
nameLength: userData.name?.length,
charCodes: userData.name ? [...userData.name].map(c => c.charCodeAt(0)) : []
})
if (!userData.id || !userData.email) {
- console.error('Invalid SAML user data:', userData)
+ debugError('Invalid SAML user data:', userData)
return null
}
- console.log('โœ… SAML Provider: User authenticated successfully', {
+ debugSuccess('SAML Provider: User authenticated successfully', {
id: userData.id,
email: userData.email,
name: userData.name
})
// ๐Ÿ”ฅ SAML ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ
+ debugProcess('Validating SAML user data structure...');
const isValidData = await validateSAMLUserData(userData)
+ debugLog('Validation result:', isValidData);
if (!isValidData) {
- console.error('Invalid SAML user data structure:', userData)
+ debugError('Invalid SAML user data structure:', userData)
return null
}
// ๐Ÿ”ฅ JIT (Just-In-Time) ์‚ฌ์šฉ์ž ์ƒ์„ฑ ๋˜๋Š” ์กฐํšŒ
- const dbUser = await getOrCreateSAMLUser({
+ debugProcess('Creating/getting SAML user from database...');
+ const userCreateData = {
email: userData.email,
name: userData.name,
- // companyId: userData.companyId,
- // techCompanyId: userData.techCompanyId,
- // ! domain = evcp ์ด๋ฉด vendor๊ฐ€ ๊ฐ–๋Š” companyId, techCompanyId๋Š” null
companyId: undefined,
techCompanyId: undefined,
domain: userData.domain
- })
+ };
+ debugLog('User create data:', userCreateData);
+
+ const dbUser = await getOrCreateSAMLUser(userCreateData)
+ debugLog('Database user result:', dbUser);
if (!dbUser) {
- console.error('Failed to get or create SAML user')
+ debugError('Failed to get or create SAML user')
return null
}
@@ -95,10 +120,15 @@ export function SAMLProvider(options: SAMLProviderOptions) {
imageUrl: dbUser.imageUrl, // DB์˜ ์‹ค์ œ ์ด๋ฏธ์ง€ URL
}
- console.log('โœ… SAML Provider: Returning user data to NextAuth:', userResult)
+ debugSuccess('SAML Provider: Returning user data to NextAuth:', userResult)
return userResult
} catch (error) {
- console.error('โŒ SAML Provider: Authentication failed', error)
+ debugError('SAML Provider: Authentication failed', {
+ error: error instanceof Error ? error.message : String(error),
+ stack: error instanceof Error ? error.stack : undefined,
+ errorType: typeof error,
+ credentials: credentials
+ });
return null
}
}
@@ -125,4 +155,101 @@ export function validateSAMLOptions(options: SAMLProviderOptions): boolean {
return required.every(field => field && field.length > 0)
}
+
+// SAMLProvider์˜ authorize ํ•จ์ˆ˜๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•˜๊ธฐ ์œ„ํ•œ ํ—ฌํผ
+export async function authenticateSAMLUser(userData: SAMLUser) {
+ debugLog('authenticateSAMLUser called with:', userData);
+
+ try {
+ // SAMLProvider ๋Œ€์‹  ์ง์ ‘ ๋กœ์ง ์‹คํ–‰ (Provider ๋ž˜ํผ ์—†์ด)
+ debugProcess('SAML User Authentication: Processing user data')
+
+ // ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ
+ if (!userData.id || !userData.email) {
+ debugError('Invalid SAML user data:', userData)
+ return null
+ }
+
+ debugSuccess('SAML User data validated successfully', {
+ id: userData.id,
+ email: userData.email,
+ name: userData.name
+ })
+
+ // ๐Ÿ”ฅ SAML ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ
+ debugLog('Validating SAML user data structure...');
+ const isValidData = await validateSAMLUserData(userData)
+ debugLog('Validation result:', isValidData);
+ if (!isValidData) {
+ debugError('Invalid SAML user data structure:', userData)
+ return null
+ }
+
+ // ๐Ÿ”ฅ JIT (Just-In-Time) ์‚ฌ์šฉ์ž ์ƒ์„ฑ ๋˜๋Š” ์กฐํšŒ
+ debugLog('Creating/getting SAML user from database...');
+ const userCreateData = {
+ email: userData.email,
+ name: userData.name,
+ companyId: undefined,
+ techCompanyId: undefined,
+ domain: userData.domain
+ };
+ debugLog('User create data:', userCreateData);
+
+ const dbUser = await getOrCreateSAMLUser(userCreateData)
+ debugLog('Database user result:', dbUser);
+
+ if (!dbUser) {
+ debugError('Failed to get or create SAML user')
+ return null
+ }
+
+ // DB์—์„œ ๊ฐ€์ ธ์˜จ ์‹ค์ œ ์‚ฌ์šฉ์ž ์ •๋ณด ๋ฐ˜ํ™˜
+ const userResult = {
+ id: String(dbUser.id), // DB์˜ ์‹ค์ œ ID
+ name: dbUser.name, // DB์˜ ์‹ค์ œ ์ด๋ฆ„
+ email: dbUser.email, // DB์˜ ์‹ค์ œ ์ด๋ฉ”์ผ
+ companyId: dbUser.companyId, // DB์˜ ์‹ค์ œ ํšŒ์‚ฌ ID
+ techCompanyId: dbUser.techCompanyId, // DB์˜ ์‹ค์ œ ๊ธฐ์ˆ ํšŒ์‚ฌ ID
+ domain: dbUser.domain, // DB์˜ ์‹ค์ œ ๋„๋ฉ”์ธ
+ imageUrl: dbUser.imageUrl, // DB์˜ ์‹ค์ œ ์ด๋ฏธ์ง€ URL
+ }
+
+ debugSuccess('SAML User Authentication completed:', userResult)
+ return userResult;
+
+ } catch (error) {
+ debugError('authenticateSAMLUser error:', {
+ error: error instanceof Error ? error.message : String(error),
+ stack: error instanceof Error ? error.stack : undefined,
+ userData
+ });
+ return null;
+ }
+}
+
+// NextAuth JWT ํ† ํฐ ์ƒ์„ฑ ํ—ฌํผ
+export async function createNextAuthToken(user: User): Promise<string> {
+ const token = {
+ id: user.id,
+ email: user.email,
+ name: user.name,
+ companyId: user.companyId,
+ techCompanyId: user.techCompanyId,
+ domain: user.domain,
+ imageUrl: user.imageUrl,
+ iat: Math.floor(Date.now() / 1000),
+ exp: Math.floor(Date.now() / 1000) + (30 * 24 * 60 * 60) // 30์ผ
+ };
+
+ const secret = process.env.NEXTAUTH_SECRET!;
+ return await encode({ token, secret });
+}
+
+// NextAuth ์„ธ์…˜ ์ฟ ํ‚ค ์ด๋ฆ„ ๊ฐ€์ ธ์˜ค๊ธฐ
+export function getSessionCookieName(): string {
+ return process.env.NODE_ENV === 'production'
+ ? '__Secure-next-auth.session-token'
+ : 'next-auth.session-token';
+}
\ No newline at end of file
diff --git a/app/api/auth/[...nextauth]/saml/utils.ts b/app/api/auth/[...nextauth]/saml/utils.ts
index 7dfe9581..73c00bf6 100644
--- a/app/api/auth/[...nextauth]/saml/utils.ts
+++ b/app/api/auth/[...nextauth]/saml/utils.ts
@@ -6,11 +6,12 @@ import {
import {
getSPMetadata,
} from "@/lib/saml/sp-metadata";
+import { debugLog, debugError, debugSuccess, debugProcess, debugMock } from '@/lib/debug-utils';
export interface SAMLProfile {
nameID?: string;
nameIDFormat?: string;
- attributes?: Record<string, string[]>;
+ attributes?: Record<string, string | string[]>; // ๋ฌธ์ž์—ด ๋˜๋Š” ๋ฐฐ์—ด ๋ชจ๋‘ ์ง€์›
[key: string]: unknown;
}
@@ -100,6 +101,12 @@ export async function createAuthnRequest(): Promise<string> {
"use server";
console.log("SSO STEP 2: Create AuthnRequest");
+
+ // Mock IdP ๋ชจ๋“œ ์ฒดํฌ
+ if (process.env.SAML_MOCKING_IDP === 'true') {
+ debugMock("Mock IdP mode enabled - simulating SAML response");
+ return createMockSAMLFlow();
+ }
try {
const config = createSAMLConfig();
@@ -170,7 +177,7 @@ export async function createAuthnRequest(): Promise<string> {
);
try {
- const zlib = require("zlib");
+ const zlib = await import("zlib");
const decompressed = zlib
.inflateRawSync(base64DecodedBuffer)
.toString("utf-8");
@@ -182,9 +189,9 @@ export async function createAuthnRequest(): Promise<string> {
// XML ๊ตฌ์กฐ ๋ถ„์„
const xmlLines = decompressed
.split("\n")
- .filter((line) => line.trim());
+ .filter((line: string) => line.trim());
console.log("XML ๊ตฌ์กฐ ์š”์•ฝ:");
- xmlLines.forEach((line, index) => {
+ xmlLines.forEach((line: string, index: number) => {
const trimmed = line.trim();
if (
trimmed.includes("<saml") ||
@@ -224,7 +231,7 @@ export async function createAuthnRequest(): Promise<string> {
` Callback URL: ${acsMatch ? acsMatch[1] : "์—†์Œ"}`
);
} catch (inflateError) {
- console.log("โŒ Deflate ์••์ถ• ํ•ด์ œ ์‹คํŒจ:", inflateError.message);
+ console.log("โŒ Deflate ์••์ถ• ํ•ด์ œ ์‹คํŒจ:", (inflateError as Error).message);
console.log(
" ์›๋ณธ ๋ฐ”์ด๋„ˆ๋ฆฌ ๋ฐ์ดํ„ฐ (hex):",
base64DecodedBuffer.toString("hex").substring(0, 100) + "..."
@@ -232,11 +239,11 @@ export async function createAuthnRequest(): Promise<string> {
}
}
} catch (decodeError) {
- console.log("โŒ Base64 ๋””์ฝ”๋”ฉ ์‹คํŒจ:", decodeError.message);
+ console.log("โŒ Base64 ๋””์ฝ”๋”ฉ ์‹คํŒจ:", (decodeError as Error).message);
}
}
} catch (analysisError) {
- console.log("โš ๏ธ SAML AuthnRequest ๋ถ„์„ ์ค‘ ์˜ค๋ฅ˜:", analysisError.message);
+ console.log("โš ๏ธ SAML AuthnRequest ๋ถ„์„ ์ค‘ ์˜ค๋ฅ˜:", (analysisError as Error).message);
}
console.log("โœ… SAML AuthnRequest URL generated:", {
@@ -271,9 +278,15 @@ export async function validateSAMLResponse(
timestamp: new Date().toISOString(),
});
+ // Mock IdP ๋ชจ๋“œ ์ฒดํฌ
+ if (process.env.SAML_MOCKING_IDP === 'true') {
+ debugMock("Mock IdP mode - returning mock SAML profile");
+ return createMockSAMLProfile(samlResponse);
+ }
+
// ์‹ค์ œ SAML ๊ฒ€์ฆ ์ˆ˜ํ–‰ (๊ธฐ๋ณธ๊ฐ’)
console.log(
- "๐Ÿ” Using Real SAML validation (SAML_USE_MOCKUP=false or not set)"
+ "๐Ÿ” Using Real SAML validation (SAML_MOCKING_IDP=false or not set)"
);
try {
@@ -293,11 +306,11 @@ export async function validateSAMLResponse(
throw new Error("No profile returned from SAML validation");
}
- // SAMLProfile ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜
+ // SAMLProfile ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜ (ํƒ€์ž… ์•ˆ์ „์„ฑ ํ™•๋ณด)
const samlProfile: SAMLProfile = {
- nameID: profile.nameID,
- nameIDFormat: profile.nameIDFormat,
- attributes: profile.attributes || {},
+ nameID: profile.nameID as string | undefined,
+ nameIDFormat: profile.nameIDFormat as string | undefined,
+ attributes: profile.attributes as Record<string, string | string[]> | undefined,
};
console.log("โœ… Real SAML Profile validated successfully:", {
@@ -332,71 +345,133 @@ export function mapSAMLProfileToUser(profile: SAMLProfile): SAMLUser {
attributes: profile.attributes,
});
+ // SAML attributes๋Š” ๋ฌธ์ž์—ด ๋˜๋Š” ๋ฐฐ์—ด ํ˜•ํƒœ์ผ ์ˆ˜ ์žˆ์Œ
+ const extractAttributeValue = (key: string): string | undefined => {
+ const value = profile.attributes?.[key];
+ if (Array.isArray(value)) {
+ return value.length > 0 ? value[0] : undefined;
+ }
+ return typeof value === 'string' ? value : undefined;
+ };
+
// ๊ธฐ๋ณธ์ ์œผ๋กœ nameID๋ฅผ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ attributes์—์„œ ์ถ”์ถœ
- const id =
- profile.nameID ||
- profile.attributes?.uid?.[0] ||
- profile.attributes?.employeeNumber?.[0] ||
- "";
- const email =
- profile.attributes?.email?.[0] ||
- profile.attributes?.mail?.[0] ||
- profile.nameID ||
- "";
- // UTF-8 ์ด๋ฆ„ ์ฒ˜๋ฆฌ ๊ฐœ์„ 
- let name =
- profile.attributes?.displayName?.[0] ||
- profile.attributes?.cn?.[0] ||
- profile.attributes?.name?.[0] ||
- (profile.attributes?.givenName?.[0] && profile.attributes?.sn?.[0]
- ? profile.attributes.givenName[0] + " " + profile.attributes.sn[0]
- : "") ||
- "";
+ const id = profile.nameID || extractAttributeValue('id') || extractAttributeValue('sub');
+ const email = extractAttributeValue('email') || extractAttributeValue('emailAddress');
+ const name = extractAttributeValue('name') || extractAttributeValue('displayName') || extractAttributeValue('cn');
+
+ // ํ•„์ˆ˜ ํ•„๋“œ ๊ฒ€์ฆ
+ if (!id) {
+ throw new Error('SAML profile missing required field: id (nameID)');
+ }
+ if (!email) {
+ throw new Error('SAML profile missing required field: email');
+ }
+ if (!name) {
+ throw new Error('SAML profile missing required field: name');
+ }
// UTF-8 ๋ฌธ์ž์—ด ์ •๊ทœํ™” ๋ฐ ๊ฒ€์ฆ
- if (name && typeof name === "string") {
- name = name.normalize("NFC").trim();
-
- // ํ•œ๊ธ€์ด ๊นจ์ง„ ๊ฒฝ์šฐ ๊ฐ์ง€ ๋ฐ ๋กœ๊ทธ
- const hasInvalidChars = /[\uFFFD\x00-\x1F\x7F-\x9F]/.test(name);
- if (hasInvalidChars) {
- console.warn("โš ๏ธ Invalid UTF-8 characters detected in name:", {
- originalName: name,
- charCodes: [...name].map((c) => c.charCodeAt(0)),
- hexDump: [...name]
- .map((c) => "\\x" + c.charCodeAt(0).toString(16).padStart(2, "0"))
- .join(""),
- });
- }
+ const normalizedName = name.normalize("NFC").trim();
+
+ // ํ•œ๊ธ€์ด ๊นจ์ง„ ๊ฒฝ์šฐ ๊ฐ์ง€ ๋ฐ ๋กœ๊ทธ
+ const hasInvalidChars = /[\uFFFD\x00-\x1F\x7F-\x9F]/.test(normalizedName);
+ if (hasInvalidChars) {
+ console.warn("โš ๏ธ Invalid UTF-8 characters detected in name:", {
+ originalName: name,
+ normalizedName,
+ charCodes: [...normalizedName].map((c) => c.charCodeAt(0)),
+ hexDump: [...normalizedName]
+ .map((c) => "\\x" + c.charCodeAt(0).toString(16).padStart(2, "0"))
+ .join(""),
+ });
}
- // ํšŒ์‚ฌ ์ •๋ณด๋Š” SSO ๋กœ๊ทธ์ธ ์‹œ ์—†์Œ
+ // ํšŒ์‚ฌ ์ •๋ณด๋Š” SSO ๋กœ๊ทธ์ธ ์‹œ ์—†์Œ (evcp ๋„๋ฉ”์ธ)
const companyId = undefined;
const techCompanyId = undefined;
const domain = 'evcp';
- const user = {
+ const user: SAMLUser = {
id,
email,
- name: name.trim(),
+ name: normalizedName,
companyId,
techCompanyId,
domain,
};
- console.log("๐Ÿ‘ค Mapped user object:", user);
+ console.log("๐Ÿ‘ค Mapped user object:", JSON.stringify(user));
return user;
}
+// Mock SAML ํ”Œ๋กœ์šฐ ์ƒ์„ฑ (ํ…Œ์ŠคํŠธ์šฉ)
+function createMockSAMLFlow(): string {
+ debugMock("Creating mock SAML flow...");
+
+ // Mock ๋ชจ๋“œ์—์„œ๋Š” Mock IdP ์—”๋“œํฌ์ธํŠธ๋กœ ๋ฆฌ๋‹ค์ด๋ ‰์…˜
+ const baseUrl = process.env.NEXTAUTH_URL || 'http://localhost:3000';
+ const mockIdpUrl = `${baseUrl}/api/auth/saml/mock-idp`;
+
+ debugMock("Mock SAML Flow - redirecting to Mock IdP:", mockIdpUrl);
+
+ return mockIdpUrl;
+}
+
+// Mock SAML Profile ์ƒ์„ฑ (ํ…Œ์ŠคํŠธ์šฉ)
+function createMockSAMLProfile(samlResponse: string): SAMLProfile {
+ console.log("๐ŸŽญ Creating mock SAML profile from response...");
+
+ try {
+ // SAML Response๊ฐ€ ์šฐ๋ฆฌ๊ฐ€ ์ƒ์„ฑํ•œ Mock์ธ์ง€ ํ™•์ธ
+ const decodedXML = Buffer.from(samlResponse, 'base64').toString('utf-8');
+ const isMockResponse = decodedXML.includes('MockIdP');
+
+ if (!isMockResponse) {
+ console.warn("โš ๏ธ Mock mode enabled but received non-mock SAML Response");
+ }
+
+ console.log("๐ŸŽญ Mock SAML XML preview:", decodedXML.substring(0, 200) + "...");
+ } catch (error) {
+ console.warn("โš ๏ธ Could not decode SAML Response for mock analysis:", (error as Error).message);
+ }
+
+ // Mock SAML Profile ๋ฐ˜ํ™˜ (์‹ค์ œ SAML Response์™€ ์ผ์น˜ํ•˜๋„๋ก ๋ฌธ์ž์—ด ํ˜•ํƒœ)
+ const mockProfile: SAMLProfile = {
+ nameID: "testuser@samsung.com",
+ nameIDFormat: "urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress",
+ attributes: {
+ email: "testuser@samsung.com",
+ name: "ํ…Œ์ŠคํŠธ ์‚ฌ์šฉ์ž",
+ displayName: "Test User Samsung",
+ // ์ถ”๊ฐ€ ํ…Œ์ŠคํŠธ ์†์„ฑ๋“ค
+ department: "๊ฐœ๋ฐœํŒ€",
+ employeeId: "TEST001",
+ mobile: "010-1234-5678"
+ }
+ };
+
+ console.log("๐ŸŽญ Mock SAML Profile created:", {
+ nameID: mockProfile.nameID,
+ nameIDFormat: mockProfile.nameIDFormat,
+ attributeCount: Object.keys(mockProfile.attributes || {}).length,
+ attributes: Object.keys(mockProfile.attributes || {}),
+ timestamp: new Date().toISOString(),
+ });
+
+ return mockProfile;
+}
+
// SAML ๋กœ๊ทธ์•„์›ƒ URL ์ƒ์„ฑ (์„œ๋ฒ„ ์•ก์…˜)
// ๋กœ๊ทธ์•„์›ƒ ์ง€์› ์•ˆํ•จ. ์ผ๋‹จ ๊ตฌ์กฐ๋งŒ ์œ ์‚ฌํ•˜๊ฒŒ ์ž‘์„ฑํ•ด๋‘ .
export async function createLogoutRequest(nameID: string): Promise<string> {
"use server";
const saml = new SAML(createSAMLConfig());
+ // Profile ๊ฐ์ฒด ํ˜•ํƒœ๋กœ ์ „๋‹ฌ
+ const profile = { nameID };
return await saml.getLogoutUrlAsync(
- nameID,
+ profile,
"", // RelayState
{
nameIDFormat: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",